/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
    BaseType,
    ExcludeDecorator,
    ExposeDecorator,
    FieldType,
    SRClass,
    SRClassType,
    SRDecorator,
    SRDiscriminator,
    SRExposeName,
    SRField,
    SRFieldRef,
    SRFile,
    SRMethod,
    SRNamespace,
    SROption,
    SRPassThrough,
    SRPostProcess,
    SRScene,
    SRStmt,
    SubDiscriminatorType,
    THIS,
    TypeDecorator,
    UnclearDecorator,
} from './SRScene';
import {
    ArkClass,
    ArkField,
    ArkFile,
    ArkInstanceFieldRef,
    ArkMethod,
    ArkNamespace,
    ArrayType,
    BooleanType,
    ClassType,
    Constant,
    Decorator,
    ImportInfo,
    NumberType,
    Scene,
    SceneConfig,
    Stmt,
    StringType,
    Type,
    UnclearReferenceType,
    UnionType,
} from '../lib/arkanalyzer/src';
import { SRConfig } from './SRConfig';
import { LogLevel } from './logger';
import { ClassSignature } from '../lib/arkanalyzer/src/core/model/ArkSignature';

const dummyTargetProjectName = 'dummy';
const logPath: string = './out/SRSceneBuilder.log';

export class SRSceneBuilder {
    private scene: Scene
    private reverseFileMap: Map<SRFile, ArkFile>
    private classMap: Map<ArkClass, SRClass>
    private fieldMap: Map<ArkField, SRField>

    constructor() {
        this.reverseFileMap = new Map()
        this.classMap = new Map()
        this.fieldMap = new Map()
    }

    build(srConfig: SRConfig): SRScene {
        const sceneConfig = new SceneConfig();
        sceneConfig.buildConfig(dummyTargetProjectName, srConfig.getProjectPath(), logPath,
            LogLevel[srConfig.getLogLevel()], [], srConfig.getTargetFiles());
        const scene = new Scene();
        scene.buildSceneFromProjectDir(sceneConfig);
        scene.inferTypes();
        this.scene = scene;

        const sRScene = new SRScene();
        scene.getFiles().forEach((arkFile) => {
            // generate file
            sRScene.addFile(this.buildFile(arkFile));
        });

        this.buildClassHierarchy()
        this.buildFieldDecorators()
        return sRScene;
    }

    private buildFile(arkFile: ArkFile): SRFile {
        const srFile = new SRFile(arkFile.getName());
        srFile.setFilePath(arkFile.getFilePath());
        arkFile.getNamespaces().forEach(namespace => srFile.addNamespace(this.buildNamespace(namespace)));
        arkFile.getClasses().forEach((arkClass: ArkClass) => {
            const srClass = this.buildClass(arkClass);
            srFile.addClass(srClass);
            srClass.setDeclaringFile(srFile)
            if (!srFile.getDefaultClass() && arkClass.isDefaultArkClass()) {
                srFile.setDefaultClass(srClass);
            }
        });
        arkFile.getImportInfos().forEach((importInfo: ImportInfo) => {
            let importSource = importInfo.getImportFrom();
            let elementName = importInfo.getImportClauseName();
            const nameBeforeAs = importInfo.getNameBeforeAs();
            if (nameBeforeAs) {
                elementName = nameBeforeAs + ' as ' + elementName;
            }
            srFile.addImportInfo(importSource, elementName, importInfo.getImportType() == 'NamedImports');
        });
        this.reverseFileMap.set(srFile, arkFile)

        return srFile;
    }

    private buildNamespace(arkNamespace: ArkNamespace): SRNamespace {
        const srNamespace = new SRNamespace(arkNamespace.getName());
        arkNamespace.getClasses().forEach((arkClass: ArkClass) => {
            const srClass = this.buildClass(arkClass);
            srNamespace.addClass(srClass);
            // TODO: add declaring namespace to srClass
            if (!srNamespace.getDefaultClass() && arkClass.isDefaultArkClass()) {
                srNamespace.setDefaultClass(srClass);
            }
        });
        arkNamespace.getNamespaces().forEach(subNamespace => srNamespace.addNamespace(this.buildNamespace(subNamespace)));
        return srNamespace;
    }

    private buildClass(arkClass: ArkClass): SRClass {
        const originType = arkClass.getOriginType();
        let srClass = new SRClass(arkClass.getName(), this.transformStrToClassType(originType));

        arkClass.getModifiers().forEach((modifier) => {
            if (!(modifier instanceof Decorator)) {
                srClass.addModifier(this.transformModifierKeyword(modifier))
            } else {
                srClass.addDecorator(this.buildDecorator(modifier))
            }
        });

        arkClass.getMethods().forEach((arkMethod) => {
            srClass.addMethod(this.buildMethod(arkMethod));
        });

        arkClass.getFields().forEach((arkField) => {
            let srField = this.buildField(arkField)
            srClass.addField(srField);
            srField.setDeclaringClass(srClass)
        });
        srClass.setOriginalCode(arkClass.getCode())

        this.classMap.set(arkClass, srClass);

        return srClass;
    }

    private buildMethod(arkMethod: ArkMethod): SRMethod {
        const srMethod = new SRMethod(arkMethod.getName());
        const modifiers: string[] = [];
        arkMethod.getModifiers().forEach((modifier) => {
            modifiers.push(this.transformModifierKeyword(modifier.toString()));
        });
        srMethod.setModifiers(modifiers);
        srMethod.setReturnType(arkMethod.getReturnType().toString());
        const parameters: string[] = [];
        arkMethod.getParameters().forEach((parameter) => {
            parameters.push(parameter.getName());
        });
        srMethod.setParameters(parameters);
        srMethod.setOriginalCode(arkMethod.getCode());

        const body = arkMethod.getBody();
        const cfg = body.getCfg();
        const originalCfg = body.getOriginalCfg();
        const stmtToOriginalStmt = body.getStmtToOriginalStmt();
        const originalStmtToSRStmt: Map<Stmt, SRStmt> = new Map();
        const srStmts: SRStmt[] = [];
        for (const originalStmt of originalCfg.getStmts()) {
            const srStmt = new SRStmt(originalStmt.toString());
            originalStmtToSRStmt.set(originalStmt, srStmt);
            srStmts.push(srStmt);
        }
        for (const stmt of cfg.getStmts()) {
            const originalStmt = stmtToOriginalStmt.get(stmt);
            if (!originalStmt) {
                continue;
            }
            const srStmt = originalStmtToSRStmt.get(originalStmt);
            if (!srStmt) {
                continue;
            }
            for (const use of stmt.getUses()) {
                if (use instanceof ArkInstanceFieldRef) {
                    // only handle field of this class
                    if (use.getBase().getName() == THIS) {
                        const srFieldRef = new SRFieldRef(use.getFieldName());
                        srStmt.addFieldRef(srFieldRef);
                    }
                }
            }
        }
        srMethod.setStatements(srStmts);

        return srMethod;
    }

    private buildField(arkField: ArkField): SRField {
        let srField = new SRField(arkField.getName());
        // set initializer
        srField.setInitializer(SRField.extractRightSideOfAssignment(arkField.getCode()));

        srField.setIsOptional(arkField.getQuestionToken())
        let srFieldType: FieldType = this.buildFieldType(arkField.getType())

        // Special case for Enum attributes
        if (srFieldType.category == BaseType.ENUM) {
            const initializer = arkField.getInitializer()
            if (initializer != undefined) {
                const initializerType = initializer.getType()
                if (initializerType instanceof NumberType) {
                    srFieldType.category = BaseType.NUMBER
                } else if (initializerType instanceof StringType) {
                    srFieldType.category = BaseType.STRING
                }
            }
        }

        srField.setType(srFieldType);
        this.fieldMap.set(arkField, srField)

        return srField;
    }

    private buildFieldDecorators() {
        this.fieldMap.forEach((srField, arkField) => {
            arkField.getModifiers().forEach((modifier) => {
                if (modifier instanceof Decorator) {
                    // decorator
                    srField.addDecorator(this.buildDecorator(modifier, srField));
                } else {
                    srField.addModifier(this.transformModifierKeyword(modifier));
                }
            });
        })
    }

    private buildDecorator(decorator: Decorator, srField?: SRField): SRDecorator {
        const decoratorType = decorator.getKind()
        const decoratorParam = decorator.getParam()
        const decoratorContent = decorator.getContent()
        let srOption: SROption | undefined = 
            srField ? this.buildOption(decoratorType, decoratorContent, srField) : undefined
        let srDecorator: SRDecorator
    
        if (decoratorType == "Type" && srOption) {
            srDecorator = new TypeDecorator(decoratorParam, srOption as SRDiscriminator)
        } else if (decoratorType == "Expose" && srOption) {
            srDecorator = new ExposeDecorator(decoratorType, srOption as SRExposeName)
        } else if (decoratorType == "Exclude") {
            srDecorator = new ExcludeDecorator(decoratorType)
        } else {
            srDecorator = new UnclearDecorator(decoratorContent)
        }
        return srDecorator
    }
    

    private buildOption(decoratorType: string, decoratorContent: string, srField: SRField): SROption | undefined {
        let priminaryJson = this.extractJson(decoratorContent)

        if (priminaryJson == "") {
            return undefined
        }

        let fixedJson = priminaryJson.replace(/([a-zA-Z_]\w*)(?=\s*:)/g, '"$1"')
        fixedJson = fixedJson.replace(/:\s*'([^']*)'/g, ':"$1"')
        fixedJson = fixedJson.replace(/:\s*([A-Za-z_]\w*)(?=[,}])/g, ':"$1"')
        fixedJson = fixedJson.replace(/,\s*([}\]])/g, '$1')

        // console.log(fixedJson)
        let parseData = JSON.parse(fixedJson)

        switch(decoratorType) {
            case "Type":
                const jsonKey = Object.keys(parseData)[0]
                if (jsonKey == "discriminator") {
                    let property: string = parseData.discriminator.property
                    let subTypes = parseData.discriminator.subTypes as { value: string; name: string }[];
                    let subClasses: SubDiscriminatorType[] = []
                    subTypes.forEach((item) => {
                        const declaringFile = srField.getDeclaringClass().getDeclaringFile()
                        let valueClass: SRClass | undefined = declaringFile.getClassByName(item.name)
                        if (!valueClass) {
                            const file = this.reverseFileMap.get(declaringFile)!
                            const importType = file.getImportInfoBy(item.name)?.
                                getLazyExportInfo()?.getTypeSignature() as ClassSignature
                            valueClass = this.classMap.get(this.scene.getClass(importType)!)
                        }
                        subClasses.push({
                            name: item.name,
                            value: valueClass!
                        })
                    })

                    return new SRDiscriminator(property, subClasses)
                } else if (jsonKey == "postProcess") {
                    const postProcess: boolean = parseData.postProcess
                    return new SRPostProcess(postProcess)
                } else if (jsonKey == "passThrough") {
                    const passThrough: boolean = parseData.passThrough
                    return new SRPassThrough(passThrough)
                } else {
                    return undefined
                }
            case "Expose":
                const name = parseData.name
                return new SRExposeName(name)
            default:
                return undefined
        }
    }

    private buildFieldType(fieldType: Type): FieldType {
        const fieldCategory: BaseType = this.transformStrToFieldType(fieldType);
        const fieldName: string = this.transformFieldName(fieldType, fieldCategory);
        const fieldGenerics: FieldType[] = [];

        if (fieldType instanceof UnclearReferenceType && fieldCategory != BaseType.OTHERS) {
            const generics = fieldType.getGenericTypes();
            generics.forEach((generic) => {
                fieldGenerics.push(this.buildFieldType(generic));
            });
        } else if (fieldType instanceof ArrayType) {
            const baseType = fieldType.getBaseType();
            fieldGenerics.push(this.buildFieldType(baseType));
        } else if (fieldType instanceof UnionType) {
            fieldType.getTypes().forEach((type) => {
                fieldGenerics.push(this.buildFieldType(type));
            });
        }
        let srType: FieldType = {
            category: fieldCategory,
            name: fieldName,
            genericTypes: fieldGenerics,
        };

        return srType;
    }

    private buildClassHierarchy() {
        this.classMap.forEach((srClass, arkClass) => {
            const superClass = arkClass.getSuperClass();
            if (superClass) {
                srClass.setSuperClass(this.classMap.get(superClass)!);
            }

            const implementInterfaceNames = arkClass.getImplementedInterfaceNames();
            implementInterfaceNames.forEach((interfaceName) => {
                const arkInterface = arkClass.getExtendedClasses().get(interfaceName);

                if (arkInterface == undefined) {
                    return;
                }

                srClass.addImplementedClass(
                    this.classMap.get(arkInterface)!,
                );
                // TODO: get interface class
            });
        });
    }

    private transformStrToFieldType(type: Type): BaseType {
        if (type instanceof BooleanType) {
            return BaseType.BOOLEAN;
        } else if (type instanceof StringType) {
            return BaseType.STRING;
        } else if (type instanceof NumberType) {
            return BaseType.NUMBER;
        } else if (type instanceof ClassType) {
            const arkClass = this.scene.getClass(type.getClassSignature());
            if (arkClass == null || arkClass!.getOriginType() != 'Enum') {
                return BaseType.CLASS;
            } else {
                return BaseType.ENUM;
            }
        } else if (type instanceof ArrayType) {
            return BaseType.ARRAY;
        } else if (type instanceof UnionType) {
            return BaseType.UNION;
        } else if (type instanceof UnclearReferenceType) {
            switch (type.getName()) {
                case 'Set':
                    return BaseType.SET;
                case 'collections.Set':
                    return BaseType.COLLECTIONS_SET;
                case 'Map':
                    return BaseType.MAP;
                case 'collections.Map':
                    return BaseType.COLLECTIONS_MAP;
                case 'collections.Array':
                    return BaseType.COLLECTIONS_ARRAY;
                case 'BigIntKeyword':
                    return BaseType.BIGINIT;
                case 'ObjectKeyword':
                    return BaseType.OBJECT;
                default:
                    return BaseType.OTHERS;
            }
        }
        return BaseType.OTHERS;
    }

    private transformFieldName(fieldType: Type, fieldCategory: BaseType) {
        switch (fieldCategory) {
            case BaseType.BIGINIT:
            case BaseType.ARRAY:
            case BaseType.MAP:
            case BaseType.SET:
            case BaseType.UNION:
                if (fieldType.toString().match('BigIntKeyword')) {
                    return fieldType.toString().replace('BigIntKeyword', 'bigint');
                } else if (fieldType.toString().match('ObjectKeyword')) {
                    return fieldType.toString().replace('ObjectKeyword', 'object');
                }
                return fieldType.toString();
            case BaseType.CLASS:
            case BaseType.ENUM:
                const parts = fieldType.toString().split(':');
                if (parts.length > 1) {
                    return parts[parts.length - 1].trim();
                } else {
                    return fieldType.toString();
                }
            default:
                return fieldType.toString();
        }
    }

    private transformStrToClassType(str: string): SRClassType {
        switch (str) {
            case 'Class':
                return SRClassType.CLASS;
            case 'Struct':
                return SRClassType.STRUCT;
            case 'Interface':
                return SRClassType.INTERFACE;
            case 'Enum':
                return SRClassType.ENUM;
            default:
                return SRClassType.OBJECT;
        }
    }

    private transformModifierKeyword(keyword: string): string {
        switch(keyword) {
            case "ExportKeyword":
                return "export";
            case "PublicKeyword":
                return "public";
            case "ProtectedKeyword":
                return "protected";
            case "PrivateKeyword":
                return "private";
            case "ReadonlyKeyword":
                return "readonly";
            case "StaticKeyword":
                return "static";
            case "AsyncKeyword":
                return "async";
            case "AbstractKeyword":
                return "abstract";
            case "DefaultKeyword":
                return "default";
            case "ConstKeyword":
                return "const";
            default:
                return keyword
        }
    }
    

    private extractJson(decorator: string): string {
        const start = decorator.indexOf('{');
        if (start === -1) {
            return '';
        }

        let depth = 0;
        for (let i = start; i < decorator.length; i++) {
            if (decorator[i] === '{') depth++;
            if (decorator[i] === '}') depth--;

            if (depth === 0) {
                return decorator.substring(start, i + 1);
            }
        }

        return '';
    }
}